/**
 * LoggingMap
 *
 * @author Chris Pratt
 *
 * 11/10/2011
 */
package com.anodyzed.onyx.log;

import java.util.Collection;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingMap<K,V> implements Map<K,V> {
  private static final Logger log = LoggerFactory.getLogger(LoggingMap.class);
  
  private String name;
  private Map<K,V> map;

  /**
   * Constructor
   *  
   * @param name The Map Name 
   * @param map The Map Access to be Logged
   */
  public LoggingMap (String name,Map<K,V> map) {
    this.name = name;
    this.map = map;
  } //LoggingMap

  /**
   * Get the Unwrapped Map
   *
   * @return The wrapped Map
   */
  public Map<K,V> unwrap () {
    return map;
  } //unwrap

  /**
   * Get the Logging Map Name
   *
   * @return Map Name
   */
  public String getName () {
    return name;
  } //getName

  /**
   * Returns the number of key-value mappings in this map. If the map contains 
   * more than Integer.MAX_VALUE elements, returns Integer.MAX_VALUE. 
   *
   * @return the number of key-value mappings in this map
   */
  @Override
  public int size () {
    int size = map.size();
    log.debug("{}.size() = {}",name,size);
    return size;
  } //size

  /**
   * Returns true if this map contains no key-value mappings.
   *
   * @return true if this map contains no key-value mappings
   */
  @Override
  public boolean isEmpty () {
    boolean empty = map.isEmpty();
    log.debug("{}.isEmpty() = {}",name,empty);
    return empty;
  } //isEmpty

  /**
   * Returns true if this map contains a mapping for the specified key. More 
   * formally, returns true if and only if this map contains a mapping for a key 
   * k such that (key==null ? k==null : key.equals(k)). (There can be at most 
   * one such mapping.) 
   *
   * @param key key whose presence in this map is to be tested
   * @return true if this map contains a mapping for the specified key 
   */
  @Override
  public boolean containsKey (Object key) {
    boolean contained = map.containsKey(key);
    log.debug("{}.containsKey({}) = {}",new Object[] {name,key,contained});
    return contained;
  } //containsKey

  /**
   * Returns true if this map maps one or more keys to the specified value. More 
   * formally, returns true if and only if this map contains at least one 
   * mapping to a value v such that (value==null ? v==null : value.equals(v)). 
   * This operation will probably require time linear in the map size for most 
   * implementations of the Map interface. 
   *
   * @param value value whose presence in this map is to be tested 
   * @return true if this map maps one or more keys to the specified value
   */
  @Override
  public boolean containsValue (Object value) {
    boolean contained = map.containsValue(value);
    log.debug("{}.containsValue({}) = {}",new Object[] {name,value,contained});
    return contained;
  } //containsValue

  /**
   * Returns the value to which the specified key is mapped, or null if this map
   * contains no mapping for the key. 
   * <p> 
   * More formally, if this map contains a mapping from a key k to a value v 
   * such that (key==null ? k==null : key.equals(k)), then this method returns 
   * v; otherwise it returns null. (There can be at most one such mapping.) 
   * <p> 
   * If this map permits null values, then a return value of null does not 
   * necessarily indicate that the map contains no mapping for the key; it's 
   * also possible that the map explicitly maps the key to null. The containsKey 
   * operation may be used to distinguish these two cases. 
   *
   * @param key the key whose associated value is to be returned 
   * @return the value to which the specified key is mapped, or null if this map 
   *         contains no mapping for the key
   */
  @Override
  public V get (Object key) {
    V got = map.get(key);
    log.debug("{}.get({}) = {}",new Object[] {name,key,got});
    return got;
  } //get

  /**
   * Associates the specified value with the specified key in this map (optional 
   * operation). If the map previously contained a mapping for the key, the old 
   * value is replaced by the specified value. (A map m is said to contain a 
   * mapping for a key k if and only if m.containsKey(k) would return true.) 
   *
   * @param key key with which the specified value is to be associated 
   * @param value value to be associated with the specified key 
   * @return the previous value associated with key, or null if there was no 
   *         mapping for key. (A null return can also indicate that the map
   *         previously associated null with key, if the implementation supports
   *         null values.)
   */
  @Override
  public V put (K key,V value) {
    V putted = map.put(key,value);
    log.debug("{}.put({},{}) = {}",new Object[] {name,key,value,putted});
    return putted;
  } //put

  /**
   * Removes the mapping for a key from this map if it is present (optional 
   * operation). More formally, if this map contains a mapping from key k to 
   * value v such that (key==null ? k==null : key.equals(k)), that mapping is 
   * removed. (The map can contain at most one such mapping.) 
   * <p> 
   * Returns the value to which this map previously associated the key, or null 
   * if the map contained no mapping for the key. 
   * <p> 
   * If this map permits null values, then a return value of null does not 
   * necessarily indicate that the map contained no mapping for the key; it's 
   * also possible that the map explicitly mapped the key to null. 
   * <p> 
   * The map will not contain a mapping for the specified key once the call 
   * returns. 
   *
   * @param key key whose mapping is to be removed from the map
   * @return the previous value associated with key, or null if there was no 
   *         mapping for key.
   */
  @Override
  public V remove (Object key) {
    V removed = map.remove(key);
    log.debug("{}.remove({}) = {}",new Object[] {name,key,removed});
    return removed;
  } //remove

  /**
   * Copies all of the mappings from the specified map to this map (optional 
   * operation). The effect of this call is equivalent to that of calling put(k, 
   * v) on this map once for each mapping from key k to value v in the specified 
   * map. The behavior of this operation is undefined if the specified map is 
   * modified while the operation is in progress. 
   *
   * @param m mappings to be stored in this map
   */
  @Override
  public void putAll (Map<? extends K,? extends V> m) {
    log.debug("{}.putAll([{} values])",name,m.size());
    map.putAll(m);
  } //putAll

  /**
   * Removes all of the mappings from this map (optional operation). The map 
   * will be empty after this call returns. 
   */
  @Override
  public void clear () {
    log.debug("{}.clear() = {} values",name,map.size());
    map.clear();
  } //clear

  /**
   * Returns a Set view of the keys contained in this map. The set is backed by 
   * the map, so changes to the map are reflected in the set, and vice-versa. If 
   * the map is modified while an iteration over the set is in progress (except 
   * through the iterator's own remove operation), the results of the iteration 
   * are undefined. The set supports element removal, which removes the 
   * corresponding mapping from the map, via the Iterator.remove, Set.remove, 
   * removeAll, retainAll, and clear operations. It does not support the add or 
   * addAll operations. 
   *
   * @return a set view of the keys contained in this map
   */
  @Override
  public Set<K> keySet () {
    Set<K> keys = map.keySet();
    log.debug("{}.keySet() = {} keys",name,keys.size());
    return keys;
  } //keySet

  /**
   * Returns a Collection view of the values contained in this map. The 
   * collection is backed by the map, so changes to the map are reflected in the 
   * collection, and vice-versa. If the map is modified while an iteration over 
   * the collection is in progress (except through the iterator's own remove 
   * operation), the results of the iteration are undefined. The collection 
   * supports element removal, which removes the corresponding mapping from the 
   * map, via the Iterator.remove, Collection.remove, removeAll, retainAll and 
   * clear operations. It does not support the add or addAll operations. 
   *
   * @return a collection view of the values contained in this map
   */
  @Override
  public Collection<V> values () {
    Collection<V> values = map.values();
    log.debug("{}.values() = {} values",name,values.size());
    return values;
  } //values

  /**
   * Returns a Set view of the mappings contained in this map. The set is backed 
   * by the map, so changes to the map are reflected in the set, and vice-versa. 
   * If the map is modified while an iteration over the set is in progress 
   * (except through the iterator's own remove operation, or through the 
   * setValue operation on a map entry returned by the iterator) the results of 
   * the iteration are undefined. The set supports element removal, which 
   * removes the corresponding mapping from the map, via the Iterator.remove, 
   * Set.remove, removeAll, retainAll and clear operations. It does not support 
   * the add or addAll operations. 
   *
   * @return a set view of the mappings contained in this map
   */
  @Override
  public Set<Map.Entry<K,V>> entrySet () {
    Set<Map.Entry<K,V>> entries = map.entrySet();
    log.debug("{}.entrySet() = {} entries",name,entries.size());
    return entries;
  } //entrySet

  /**
   * Wrap the supplied Map in a Logging Map <b>IF</b> the current logging level 
   * of the calling class is Debug or above. 
   *  
   * NB: The logging level is checked against the name of the calling class. 
   * If some other log criteria is being used it will have to be checked 
   * manually. 
   *  
   * NB: The debug log level is evaluated only once, when wrap is called.  If 
   * the logging level changes after the map has been wrapped, no change will be 
   * observed by the Map returned from this call. 
   *  
   * @param name The Map Name 
   * @param map The Map to be wrapped
   * @return The wrapped Map or the original map if debug is not enabled
   */
  public static <K,V> Map<K,V> wrap (String name,Map<K,V> map) {
    Log log = LogBuilder.getLogger(Thread.currentThread().getStackTrace()[2].getClassName());
    if(log.isDebugEnabled()) {
      return new LoggingMap<K,V>(name,map);
    }
    return map;
  } //wrap

} //*LoggingMap
